%% =======================================================================================
%% FTP Sync.
%% Copyright Lorin Liu. All Rights Reserved.
%% =======================================================================================

-module(ftpsync).

-behaviour(gen_server).

%% =======================================================================================
%% Export functions

%% API
-export([start/1, stop/0]).
-export([sync/0, sync_l2r/0, sync_r2l/0, state/0, config/0, config/1]).

%% Behaviour gen_server callbacks
-export([init/1, terminate/2, code_change/3]).
-export([handle_info/2, handle_cast/2, handle_call/3]).

%% =======================================================================================
%% API

start(Config) when is_list(Config) ->
    inets:start(),
    gen_server:start({local, ?MODULE}, ?MODULE, Config, []).

stop() ->
    gen_server:cast(?MODULE, stop),
    inets:stop().

%% ---------------------------------------------------------------------------------------

sync() ->
    sync_l2r().

sync_l2r() ->
    gen_server:call(?MODULE, sync_local_to_remote, infinity).

sync_r2l() ->
    gen_server:call(?MODULE, sync_remote_to_local, infinity).

state() ->
    gen_server:call(?MODULE, state).

config() ->
    gen_server:call(?MODULE, config, infinity).

config(Config) when is_list(Config) ->
    gen_server:call(?MODULE, {config, Config}, infinity).

%% =======================================================================================
%% Internal definitions

-record(state,
    {
        config           = undefined,
        host             = undefined,
        port             = 21,
        timeout          = 60000,
        user             = "anonymous",
        password         = "",
        remote_top_dir   = "",
        remote_dir       = ".",
        local_dir        = ".",
        file_list_config = undefined
    }).

-record(file,
    {
        name       = undefined,
        remote_dir = undefined,
        local_dir  = undefined,
        sync_state = undefined
    }).

%% =======================================================================================
%% Behaviour gen_server callbacks

init(Config) ->
    {ok, parse_config(Config)}.

terminate(normal, _State) ->
    ok.

code_change(_OldVsn, _State, _Extra) ->
    {error, unsupported}.

%% ---------------------------------------------------------------------------------------

handle_info(_Info, State) ->
    {noreply, State}.

handle_cast(stop, State) ->
    {stop, normal, State};

handle_cast(_Msg, State) ->
    {noreply, State}.

%% ---------------------------------------------------------------------------------------

handle_call(sync_local_to_remote, _From, State) ->
    {ok, Reply} = sync(sync_local_to_remote, State),
    {reply, {ok, Reply}, State};

handle_call(sync_remote_to_local, _From, State) ->
    {ok, Reply} = sync(sync_remote_to_local, State),
    {reply, {ok, Reply}, State};

handle_call(state, _From, State) ->
    {reply, {ok, State}, State};

handle_call(config, _From, State) ->
    {reply, ok, parse_config(State#state.config)};

handle_call({config, Config}, _From, _State) ->
    {reply, ok, parse_config(Config)};

handle_call(Msg, _From, State) ->
    {reply, {error, unsupported, Msg}, State}.

%% =======================================================================================
%% Internal functions

normalize_string(Str) ->
    string:strip(string:strip(string:strip(Str, both, $\n), both, $\s), right, $/).

split_string(Str, SubStr) ->
    lists:split(string:len(SubStr), Str).

%% ---------------------------------------------------------------------------------------

parse_config(Config) when is_list(Config) ->
    {ok, ConfigOptions} = file:consult(Config),
    lists:foldl(fun parse_config/2, #state{config = Config}, ConfigOptions).

parse_config({ConfigOption, Value}, State) when is_atom(ConfigOption) ->
    case ConfigOption of
        host ->
            State#state{host = Value};
        port ->
            State#state{port = Value};
        timeout ->
            State#state{timeout = Value};
        user ->
            State#state{user = Value};
        password ->
            State#state{password = Value};
        remote_top_dir ->
            State#state{remote_top_dir = normalize_string(Value)};
        remote_dir ->
            State#state{remote_dir = normalize_string(Value)};
        local_dir ->
            State#state{local_dir = normalize_string(Value)};
        file_list_config ->
            State#state{file_list_config = Value};
        _ ->
            State
    end;

parse_config(_, State) ->
    State.

%% ---------------------------------------------------------------------------------------

apply_file_fun(File, Fun) ->
    case catch Fun(File) of
        {ok, ResultFile} ->
            ResultFile#file{sync_state = ok};
        {error, Error} ->
            File#file{sync_state = {error, Error}};
        Error ->
            File#file{sync_state = {error, Error}}
    end.

%% ---------------------------------------------------------------------------------------

init_file_list(FileListConfig) ->
    {ok, Fd} = file:open(FileListConfig, [read, read_ahead, raw]),
    {ok, FileList} = init_file_list([], Fd),
    ok = file:close(Fd),
    {ok, FileList}.

init_file_list(FileList, Fd) ->
    case file:read_line(Fd) of
        {ok, [$# | _Data]} ->
            init_file_list(FileList, Fd);
        {ok, Data} ->
            case Line = normalize_string(Data) of
                [] ->
                    init_file_list(FileList, Fd);
                _ ->
                    File = #file{name = filename:basename(Line),
                                 remote_dir = filename:dirname(Line)},
                    init_file_list([File | FileList], Fd)
            end;
        eof ->
            {ok, lists:reverse(FileList)}
    end.

%% ---------------------------------------------------------------------------------------

validate_file(File, RemoteTopDir, RemoteDir, LocalDir) ->
    {RemoteDirPrefix, Dir} = split_string(File#file.remote_dir, RemoteDir),
    case RemoteDir =:= RemoteDirPrefix of
        true ->
            {ok, File#file{remote_dir = RemoteTopDir ++ File#file.remote_dir,
                           local_dir = LocalDir ++ Dir}}
    end.

validate_file_list(FileList, RemoteTopDir, RemoteDir, LocalDir) ->
    Fun = fun(File) ->
              apply_file_fun(File,
                             fun(F) ->
                                 validate_file(F, RemoteTopDir, RemoteDir, LocalDir)
                             end)
          end,
    {ok, lists:map(Fun, FileList)}.

%% ---------------------------------------------------------------------------------------

sync_file(#file{sync_state = {error, Error}}, _Mode, _Pid) ->
    {error, Error};

sync_file(#file{name = Name, remote_dir = RemoteDir, local_dir = LocalDir} = File,
          Mode, Pid) ->
    ok = ftp:cd(Pid, RemoteDir),
    ok = ftp:lcd(Pid, LocalDir),
    ok = ftp:type(Pid, binary),
    case Mode of
        sync_local_to_remote ->
            ok = ftp:send(Pid, Name);
        sync_remote_to_local ->
            ok = ftp:recv(Pid, Name)
    end,
    {ok, File}.

sync(Mode, #state{host = Host, port = Port, timeout = Timeout,
                  user = User, password = Password,
                  remote_top_dir = RemoteTopDir,
                  remote_dir = RemoteDir, local_dir = LocalDir,
                  file_list_config = FileListConfig})
when (Mode =:= sync_local_to_remote orelse Mode =:= sync_remote_to_local) ->
    {ok, InitList} = init_file_list(FileListConfig),
    {ok, ValidList} = validate_file_list(InitList, RemoteTopDir, RemoteDir, LocalDir),
    {ok, Pid} = inets:start(ftpc, [{host, Host}, {port, Port}, {timeout, Timeout}]),
    ok = ftp:user(Pid, User, Password),
    Fun = fun(File) ->
              apply_file_fun(File, fun(F) -> sync_file(F, Mode, Pid) end)
          end,
    ResultList = lists:map(Fun, ValidList),
    ok = inets:stop(ftpc, Pid),
    {ok, ResultList}.

%% =======================================================================================

